5.15. Модули и организация кода
Модули и организация кода
Модули и организация кода
require() — загрузка модулей.
Создание модуля
Пространства имён, избегание глобальных переменных.
По мере роста программной системы возникает необходимость структурирования кода: разделения его на логические компоненты, инкапсуляции функциональности и управления зависимостями. В Lua эта задача решается с помощью механизма модулей, который, хотя и эволюционировал со временем, сохраняет свою минималистичную природу — в полном соответствии с философией языка.
Центральным элементом организации кода в Lua является функция require, предназначенная для однократной загрузки и инициализации модуля по его имени.
local mymodule = require("mymodule")
Принцип работы
- Lua проверяет, не был ли уже загружен модуль с именем "mymodule" (в таблице package.loaded).
- Если нет — ищется файл или C-расширение, соответствующее имени, согласно путям, заданным в package.path и package.cpath.
- Найденный файл выполняется в глобальном окружении, но ожидается, что он вернёт таблицу — интерфейс модуля.
- Результат кэшируется в
package.loaded["mymodule"], чтобы последующие вызовы require возвращали тот же объект без повторного выполнения.
Важно: require гарантирует идемпотентность загрузки — модуль будет выполнен ровно один раз за сессию интерпретатора.
Как ищутся модули?
Lua использует шаблоны путей:
- package.path — для Lua-файлов (например: ./?.lua;/usr/local/lua/?.lua)
- package.cpath — для бинарных модулей (C-расширений) Символ ? заменяется на имя модуля. Например:
require("utils") -- может загрузить ./utils.lua или /usr/share/lua/5.4/utils.lua
Это позволяет реализовать гибкую систему поиска, которую можно расширять динамически.
Модуль в Lua — это любой файл, возвращающий таблицу, которая представляет собой его публичный интерфейс.
Рекомендуемый способ создания модуля:
-- файл: math_utils.lua
local M = {}
function M.square(x)
return x * x
end
function M.cube(x)
return x * x * x
end
-- Приватная функция (не экспортируется)
local function is_positive(x)
return x > 0
end
return M
Загрузка:
local math_utils = require("math_utils")
print(math_utils.square(4)) -- 16
Ранние версии Lua (до 5.2) предлагали функцию module(), автоматически оборачивающую код в глобальный модуль:
module("myoldmodule", package.seeall)
function foo() ... end
Однако этот подход загрязняет глобальное пространство, не поддерживает инкапсуляцию, и удалён в 5.3+. Поэтому всегда используйте явное возвращение таблицы.
Одна из ключевых проблем в Lua — случайное создание глобальных переменных, что может привести к конфликтам и трудноуловимым ошибкам.
function init()
counter = 0 -- ОШИБКА: создана глобальная переменная!
end
Lua по умолчанию разрешает создание глобальных переменных. Это удобно для прототипирования, но неприемлемо в продакшене.
Чтобы обнаружить случайные глобальные присваивания, используйте следующий трюк в начале файла:
-- Блокировка создания новых глобальных переменных
setmetatable(_G, {
__newindex = function(_, name, value)
error("Попытка создать глобальную переменную '" .. name .. "'", 2)
end,
__index = function(_, name)
error("Попытка прочитать несуществующую глобальную переменную '" .. name .. "'", 2)
end
})
Теперь любое обращение к необъявленной глобальной переменной вызовет ошибку — отличный способ повысить надёжность. Альтернатива: использовать строгий режим через сторонние библиотеки (strict.lua) или статические анализаторы (например, luacheck). Мы как раз об этом говорили ранее.
Для логической группировки функций и значений применяйте таблицы:
-- network/http.lua
local http = {}
function http.get(url)
...
end
function http.post(url, data)
...
end
return http
-- main.lua
local http = require("network.http")
http.get("https://example.com")
Таким образом, вы создаёте логическое пространство имён, аналогичное пакетам в Python или модулям в JavaScript.
А как модули организовать? Давайте поговорим об иерархии модулей и организации проекта.
В крупных приложениях модули организуются в древовидную структуру:
project/
├── main.lua
├── utils/
│ ├── string.lua
│ ├── table.lua
│ └── index.lua
├── game/
│ ├── player.lua
│ └── world.lua
└── config.lua
Пример иерархического доступа:
local str_util = require("utils.string")
local player = require("game.player")
Если require("utils") — и в директории utils есть init.lua или index.lua, он будет загружен как содержимое модуля:
-- utils/init.lua
return {
string = require("utils.string"),
table = require("utils.table"),
}
Теперь можно писать:
local utils = require("utils")
utils.string.trim(" hello ")
Это стандартный паттерн для создания сборных модулей. Lua допускает циклические зависимости между модулями, но они могут привести к неожиданному поведению.
-- a.lua
local B = require("b")
local A = { value = "A" }
function A.use_b() return B.value end
return A
-- b.lua
local A = require("a") -- a ещё не завершён!
local B = { value = "B" }
return B
В момент require("a") внутри b.lua, модуль a ещё не вернул таблицу — A будет nil.
Решением будет отложенная загрузка или внедрение зависимостей.
- Перенос require внутрь функций:
function B.use_a()
local A = require("a")
return A.value
end
- Инъекция зависимостей:
-- Вместо require — передача через параметры
return function(dependencies)
local A = dependencies.A
...
end
Модули в Lua кэшируются — каждый require возвращает один и тот же объект. Это означает, что модули по сути являются синглтонами. Если модуль хранит изменяемое состояние:
-- counter.lua
local count = 0
local M = {}
function M.inc() count = count + 1 end
function M.get() return count end
return M
То все части программы будут делить это состояние. Это может быть полезно (логгер, конфиг), но опасно при неосторожном использовании. По возможности делайте модули stateless (без состояния), а состояние передавайте явно.
Lua предоставляет минимальный, но достаточный набор средств. Его гибкость позволяет строить сложные системы, но ответственность за порядок лежит на разработчике.